﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.Net;
using IRCServer.Entities;
using IRCServer.Utilities;
using IRCServer.Entities.Commands;
using IRCServer.Backend;
using IRC.Utilities;
using IRC.Utilities.Entities;

namespace IRCServer
{
    class IRCServer
    {
        private NodeConfiguration localNode;
        private Socket server;
        private List<NodeConfiguration> configuration;

        public IRCServer(NodeConfiguration localNode, List<NodeConfiguration> configuration)
        {
            this.localNode = localNode;
            this.configuration = configuration;
            ServerBackend.Instance.RoutingDaemon = new RoutingDaemon(new IPEndPoint(IPAddress.Loopback, localNode.LocalPort));
        }

        public void Start()
        {
            server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            IPEndPoint iep = new IPEndPoint(IPAddress.Any, localNode.ServerPort);
            Logger.Instance.Info(string.Format("Starting server with port {0}", localNode.ServerPort));
            server.Bind(iep);
            server.Listen(5);
            this.WaitForConnections();
            Logger.Instance.Info("Server attempting to communicate with neighbors.");

            foreach (NodeConfiguration node in configuration.Where(n => n != localNode).ToList())
            {
                IPEndPoint endPoint = new IPEndPoint(IPAddress.Parse(node.Hostname), node.ServerPort);
                Socket clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                ServerSession session = new ServerSession()
                {
                    ServerNodeID = node.NodeID,
                    Socket = clientSocket,
                    Hostname = node.Hostname
                };
                clientSocket.BeginConnect(endPoint, new AsyncCallback(EndConnect), session);
            }

            Logger.Instance.Info("Server up and waiting for connections...");

            while (true)
            {
                System.Threading.Thread.Sleep(10000);
            }
        }

        private void EndConnect(IAsyncResult result)
        {
            ServerSession session = ((ServerSession)result.AsyncState);
            try
            {
                session.Socket.EndConnect(result);
                string message = CommandGenerator.GenerateCommand(CommandType.SERVER, this.localNode.NodeID.ToString(), this.localNode.Hostname);
                byte[] messageData = System.Text.Encoding.ASCII.GetBytes(message);

                if (!ServerBackend.Instance.ServerSessions.Contains(session))
                {
                    ServerBackend.Instance.ServerSessions.Add(session);
                    Logger.Instance.Warn(string.Format("Communication successful with neighbor node {0}", session.ServerNodeID));
                    session.Socket.BeginSend(messageData, 0, messageData.Length, SocketFlags.None, new AsyncCallback(FinalizeSending), session);
                    session.Socket.BeginReceive(session.Buffer, 0, session.Buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveCommand), session);
                }
            }
            catch (SocketException)
            {
                Logger.Instance.Error(string.Format("Failed to connect to neighbor node {0}", session.ServerNodeID));
            }
        }

        private void WaitForConnections()
        {
            server.BeginAccept(new AsyncCallback(AcceptConnection), server);
        }

        private void AcceptConnection(IAsyncResult result)
        {
            Socket client = server.EndAccept(result);
            Logger.Instance.Info(string.Format("Accepted connection from {0}", client.RemoteEndPoint.ToString()));
            SessionBase session = new SessionBase()
            {
                Socket = client
            };
            this.WaitForConnections();
            client.BeginReceive(session.Buffer, 0, session.Buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveCommand), session);
            Logger.Instance.Info(string.Format("Ready to receive messages from Client {0}({1})", ServerBackend.Instance.ClientSessions.Count - 1, client.RemoteEndPoint.ToString()));
        }

        private static string ClientDisplayString(SessionBase session)
        {
            if (session is ClientSession)
            {
                ClientSession clientSession = (ClientSession)session;
                if (clientSession.User != null && clientSession.User.Nickname != null)
                    return clientSession.User.Nickname;
                else
                    return string.Format("client {0}", clientSession.ClientID);
            }
            else if (session is ServerSession)
            {
                ServerSession serverSession = (ServerSession)session;
                return string.Format("server {0}", serverSession.ServerNodeID);
            }
            else
            {
                return "undefined socket";
            }
        }

        private void ReceiveCommand(IAsyncResult result)
        {
            SessionBase session = (SessionBase)result.AsyncState;
            
            int bytesReceived = 0;
            try
            {
                bytesReceived = session.Socket.EndReceive(result);
            }
            catch
            {
                Logger.Instance.Debug("Error on EndReceive");
            }

            if (bytesReceived == 0)
            {
                //Zero bytes received means the client terminated the connection
                Logger.Instance.Info(string.Format("Closing connection with {0}", ClientDisplayString(session)));
                session.Socket.Close();

                if (session is ClientSession)
                {
                    ClientSession clientSession = (ClientSession)session;
                    string quitCommand = CommandGenerator.GenerateCommand(CommandType.QUIT, "Connection terminated");
                    CommandFactory.GetCommandFromMessage(quitCommand, ref session).ExecuteCommand(session);
                    clientSession.Socket.Close();
                    ServerBackend.Instance.ClientSessions.Remove(clientSession);
                }
                else if (session is ServerSession)
                {
                    ServerSession serverSession = (ServerSession)session;
                    serverSession.Socket.Close();
                    ServerBackend.Instance.ServerSessions.Remove(serverSession);
                }
                return;
            }

            string message = System.Text.Encoding.ASCII.GetString(session.Buffer, 0, bytesReceived);
            Logger.Instance.Info(string.Format("Received '{0}' from {1}", message, ClientDisplayString(session)));
            string response = String.Empty;
            IRCCommandBase command;
            try
            {
                command = CommandFactory.GetCommandFromMessage(message, ref session);

                if (session is ClientSession)
                {
                    ClientSession clientSession = (ClientSession)session;
                    if (!ServerBackend.Instance.ClientSessions.Contains(clientSession))
                    {
                        clientSession.ClientID = ServerBackend.Instance.ClientSessions.Count;
                        ServerBackend.Instance.ClientSessions.Add(clientSession);
                    }
                    response = command.ExecuteCommand(clientSession);
                }
                else if (session is ServerSession)
                {
                    ServerSession serverSession = (ServerSession)session;
                    if (!ServerBackend.Instance.ServerSessions.Contains(serverSession))
                    {
                        Logger.Instance.Warn(string.Format("Communication successful with neighbor node {0}", serverSession.ServerNodeID));
                        ServerBackend.Instance.ServerSessions.Add(serverSession);
                    }
                    response = command.ExecuteCommand(serverSession);
                }
            }
            catch
            {
                response = Errors.GetErrorResponse(ErrorCode.ERR_UNKNOWNCOMMAND, String.Empty);
            }

            Logger.Instance.Info(string.Format("Responding with '{0}' to {1}", response, ClientDisplayString(session)));
            byte[] dataToSend = System.Text.Encoding.ASCII.GetBytes(response);
            session.Socket.BeginSend(dataToSend, 0, dataToSend.Length, SocketFlags.None, new AsyncCallback(FinalizeSending), session);
            session.Socket.BeginReceive(session.Buffer, 0, session.Buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveCommand), session);
        }

        private void FinalizeSending(IAsyncResult result)
        {
            SessionBase session = (SessionBase)result.AsyncState;
            session.Socket.EndSend(result);
        }
    }
}
